<?php
/**
 * Copyright 2011  SURFfoundation
 * 
 * This file is part of ESCAPE.
 * 
 * ESCAPE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * ESCAPE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with ESCAPE.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * For more information:
 * http://escapesurf.wordpress.com/
 * http://www.surffoundation.nl/
 * 
 * Contact: d.vierkant@utwente.nl
 * 
 * @license http://www.gnu.org/licenses/gpl.html GNU GPLv3
 */
?>
<?php
/**
 * Fedora
 * 
 * Provides a wrapper around Fedora's API-A and API-M interfaces.
 */
class fedora_Fedora
{
	protected $apiASoapClient = null;
	protected $apiMSoapClient = null;
	protected $config = null;
	
	/**
	 * Create a Fedora instance.
	 * @param array $config Fedora config array
	 */
	public function __construct($config)
	{
		$this->config = $config;
	}
	
//	/**
//	 * API-A findObjects method)
//	 * @param $resultFields
//	 * @param $maxResult
//	 * @param $query
//	 * @return unknown_type
//	 */
//	public function findObjects($resultFields, $maxResult, $query)
//	{
//		$result = null;
//		
//		$parameters = array('resultFields' => $resultFields, 'maxResults' => $maxResult, 'query' => $query);
//		
//		$client = new SoapClient($this->config['url'] . "/fedora/wsdl?api=API-A", array('features' => SOAP_SINGLE_ELEMENT_ARRAYS, 'login' => $this->config['username'], 'password' => $this->config['password']));
//		$response = $client->findObjects($parameters);
//		if($response !== null)
//		{
//			$result = $response->result;
//		}
//		
//		return $result;
//	}

	/**
	 * Get a API-A SoapClient instance.
	 * @return SoapClient API-A SoapClient
	 */
	public function &getApiASoapClient()
	{
		// cache soap clients
		if($this->apiASoapClient === null)
		{
			$this->apiASoapClient = new SoapClient($this->config['url'] . "/fedora/wsdl?api=API-A", array('login' => $this->config['username'], 'password' => $this->config['password']));;
		}
		
		return $this->apiASoapClient;
	}
	
	/**
	 * Get a API-M SoapClient instance.
	 * @return SoapClient API-M SoapClient
	 */
	public function &getApiMSoapClient()
	{
		// cache soap clients
		if($this->apiMSoapClient === null)
		{
			$this->apiMSoapClient = new SoapClient($this->config['url'] . "/fedora/wsdl?api=API-M", array('login' => $this->config['username'], 'password' => $this->config['password']));;
		}
		
		return $this->apiMSoapClient;
	}
	
	/**
	 * Gets the object state.
	 * @param string $pid PID of the object
	 * @return string A = Active, I = Inactive, D = Deleted, null if the object is not found
	 */
	public function getObjectState($pid)
	{
		$state = null;

		$spoQuery = '<' . $this->convertPidToUri($pid) . '> <info:fedora/fedora-system:def/model#state> *';
		$rdfXml = $this->queryRiSpo($spoQuery);
		$rdfDocument = new simplerdf_RdfDocument($rdfXml);
		if($rdfDocument->getResourceCount() > 0)
		{
			$stateUri = $rdfDocument->getResource(0)->getFirstObject("info:fedora/fedora-system:def/model#state");
			preg_match('~^info:fedora/fedora-system:def/model#(.)~', $stateUri, $matches);
			$state = $matches[1];
		}
		
		return $state;
	}

	/**
	 * Purge an object.
	 * @param string $pid PID of the object
	 * @param boolean $force ???
	 * @param string $logMessage Optional log message
	 * @return string timestamp
	 */
	public function purgeObject($pid, $force = false, $logMessage = "ESCAPE API purge")
	{
		$result = null;
		
		$parameters = array('pid' => $pid, 'force' => $force, 'logMessage' => $logMessage);
		
		$client = $this->getApiMSoapClient();
		$response = $client->purgeObject($parameters);
		if($response !== null)
		{
			$result = $response->purgedDate;
		}
		
		return $result;
	}
	
	/**
	 * Modifies an object.
	 * @param string $pid PID of the object
	 * @param string $state State, A = Active, I = Inactive, D = Deleted
	 * @param string $label Label
	 * @param string $ownerId Owner ID
	 * @param string $logMessage Log message
	 * @return string timestamp
	 */
	public function modifyObject($pid, $state = null, $label = null, $ownerId = null, $logMessage = "ESCAPE API modifyObject")
	{
		$result = null;
		
		$parameters = array('pid' => $pid, 'state' => $state, 'label' => $label, 'ownerId' => $ownerId, 'logMessage' => $logMessage);
		
		$client = $this->getApiMSoapClient();
		$response = $client->modifyObject($parameters);
		if($response !== null)
		{
			$result = $response->modifiedDate;
		}
		
		return $result;
	}
	
	/**
	 * Modify a datastream. A new datastream will be created if it does not exist yet.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param string $controlGroup one of "X", "M", "R", or "E" (Inline *X*ML, *M*anaged Content, *R*edirect, or *E*xternal Referenced) 
	 * @param string $dsLabel the label for the datastream 
	 * @param string $dsContent data
	 * @param string $mimeType the MIME type of the content being added
	 * @param string $logMessage a message describing the activity being performed 
	 */
	public function modifyDatastream($pid, $dsId, $controlGroup, $dsLabel, $dsContent, $mimeType, $logMessage = 'ESCAPE API update')
	{
		$params = array(
			'controlGroup' => $controlGroup,
			'mimeType' => $mimeType,
			'dsLabel' => $dsLabel,
			'logMessage' => $logMessage
		);
		
		$url = $this->config['url'] . '/fedora/objects/' . rawurlencode($pid) . '/datastreams/' . rawurlencode($dsId) . '?' . http_build_query($params);
	
		httputils_HttpUtils::doPostRequest($url, $dsContent, null, $this->config['username'], $this->config['password']);
	}

	/**
	 * Sets the state of a Datastream to the specified state value.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param string $dsState One of "A", "D", or "I" (Active, Deleted, or Inactive)
	 * @param string $logMessage a message describing the activity being performed 
	 */
	public function setDatastreamState($pid, $dsId, $dsState, $logMessage = 'ESCAPE API update')
	{
		$result = null;
		
		$parameters = array(
			'pid' => $pid,
			'dsID' => $dsId,
			'dsState' => $dsState,
			'logMessage' => $logMessage,
		);
		
		$client = $this->getApiMSoapClient();
		$response = $client->setDatastreamState($parameters);
		if($response !== null)
		{
			$result = $response->modifiedDate;
		}
		
		return $result;
	}

	/**
	 * Purge a datastream.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param string $startDateTime the (inclusive) start date-time stamp of the range. If not specified, this is taken to be the lowest possible value, and thus, the entire version history up to the endDateTime will be purged 
	 * @param string $endDateTime the (inclusive) ending date-time stamp of the range. If not specified, this is taken to be the greatest possible value, and thus, the entire version history back to the startDateTime will be purged 
	 * @param string $logMessage a message describing the activity being performed 
	 * @return string timestamp
	 */
	public function purgeDatastream($pid, $dsId, $startDateTime = null, $endDateTime = null, $logMessage = 'ESCAPE API purge')
	{
		$result = null;
		
		$parameters = array(
			'pid' => $pid,
			'dsID' => $dsId,
			'startDT' => $startDateTime,
			'endDT' => $endDateTime,
			'logMessage' => $logMessage,
			'force' => false
		);
		
		$client = $this->getApiMSoapClient();
		$response = $client->purgeDatastream($parameters);
		if($response !== null)
		{
			$result = $response->purgedVersionDate;
		}
		
		return $result;
	}
	
	/**
	 * Modifies a datastream.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param object $datastream Datastream object
	 * @return timestamp
	 */
	public function modifyDatastreamByValue($pid, $dsId, $datastream)
	{
		$result = null;
		
		$datastream->pid = $pid;
		$datastream->dsID = $dsId;
		if(@$datastream->dsLabel == null)
		{
			$datastream->dsLabel = @$datastream->label;
		}
		if(@$datastream->logMessage == null)
		{
			$datastream->logMessage = 'ESCAPE API update';
		}
		$datastream->force = false;
		
		$client = $this->getApiMSoapClient();
		$response = $client->modifyDatastreamByValue($datastream);
		if($response !== null)
		{
			$result = $response->modifiedDate;
		}
		
		return $result;
	}

	/**
	 * Modifies a datastream.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param string $dsContent Datastream content
	 * @return timestamp
	 */
	public function modifyDatastreamByValueString($pid, $dsId, $dsContent)
	{
		$timestamp = null;
		
		$datastream = $this->getDatastream($pid, $dsId);
		if($datastream !== null)
		{
			$datastream->dsContent = $dsContent;
			$timestamp = $this->modifyDatastreamByValue($pid, $dsId, $datastream);
		}
			
		return $timestamp;
	}
	
	/**
	 * List datastreams.
	 * @param $pid PID of the object
	 * @param $asOfDateTime As of datetime
	 * @return array Array of datastream definition objects
	 */
	public function listDatastreams($pid, $asOfDateTime = null)
	{
		$result = null;
		
		$parameters = array('pid' => $pid, 'asOfDateTime' => $asOfDateTime);
		
		$client = $this->getApiASoapClient();
		$datastreams = $client->listDatastreams($parameters);
		
		if($datastreams !== null)
		{
			$result = $datastreams->datastreamDef;
		}
		
		return $result;
	}
	
	/**
	 * Get a datastream.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param string $asOfDateTime As of datetime
	 * @return object Datastream 
	 */
	public function getDatastream($pid, $dsId, $asOfDateTime = null)
	{
		$result = null;
		
		$parameters = array('pid' => $pid, 'dsID' => $dsId, 'asOfDateTime' => $asOfDateTime);
		
		$client = $this->getApiMSoapClient();
		$datastream = $client->getDatastream($parameters);
		if($datastream !== null)
		{
			$result = $datastream->datastream;
		}
		
		return $result;
	}

	/**
	 * Get datastream dissemination.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param string $asOfDateTime As of datetime
	 * @return string Datastream dissemination
	 */
	public function getDatastreamDissemination($pid, $dsId, $asOfDateTime = null)
	{
		$result = null;
		
		$parameters = array('pid' => $pid, 'dsID' => $dsId, 'asOfDateTime' => $asOfDateTime);
		
		$client = $this->getApiASoapClient();
		$datastreamDissemination = $client->getDatastreamDissemination($parameters);
		if($datastreamDissemination !== null)
		{
			$result = $datastreamDissemination->dissemination;
		}
		
		return $result;
	}

	/**
	 * Sends the datastream content to the output.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param string $asOfDateTime As of datetime
	 */
	public function outputDatastreamContent($pid, $dsId, $asOfDateTime = null)
	{
		$url = $this->config['url'] . '/fedora/objects/' . rawurlencode($pid) . '/datastreams/' . rawurlencode($dsId) . '/content?' . http_build_query(array('asOfDateTime' => $asOfDateTime));
		
		httputils_HttpUtils::readFileChunked($url, true, null, $this->config['username'], $this->config['password']);
	}
	
	/**
	 * Copy the datastream content to a file stream.
	 * @param string $pid PID of the object
	 * @param string $dsId Datastream ID
	 * @param string $destinationResource Destination file stream
	 * @param string $asOfDateTime As of datetime
	 */
	public function copyDatastreamContent($pid, $dsId, $destinationResource, $asOfDateTime = null)
	{
		$url = $this->config['url'] . '/fedora/objects/' . rawurlencode($pid) . '/datastreams/' . rawurlencode($dsId) . '/content?' . http_build_query(array('asOfDateTime' => $asOfDateTime));
		
		httputils_HttpUtils::copyFileChunked($url, $destinationResource, null, $this->config['username'], $this->config['password']);
	}
	
	/**
	 * Get object profile.
	 * @param string $pid PID of the object
	 * @param string $asOfDateTime As of datetime
	 * @return object Object profile
	 */
	public function getObjectProfile($pid, $asOfDateTime = null)
	{
		$result = null;
		
		$parameters = array('pid' => $pid, 'asOfDateTime' => $asOfDateTime);
		
		$client = $this->getApiASoapClient();
		$objectProfile  = $client->getObjectProfile($parameters);
		if($objectProfile !== null)
		{
			$result = $objectProfile->objectProfile;
		}
		
		return $result;
	}
	
	/**
	 * Check if an object exists.
	 * @param string $pid PID of the object
	 * @param string $asOfDateTime As of datetime
	 * @return boolean True if the object exists
	 */
	public function getObjectExists($pid, $asOfDateTime = null)
	{
		$exists = false;
		
		$objectProfile = $this->getObjectProfile($pid, $asOfDateTime);
		if($objectProfile !== null)
		{
			$exists = true;
		}
		
		return $exists;
	}

	/**
	 * Generate one or more PIDs.
	 * @param int $numPids The number of PIDs to generate
	 * @param string $pidNamespace The namespace prefix of the PIDs (without a semicolon)
	 * @return string|array The PIDs, as a single String when numPids = 1 or as an Array when numPids > 1
	 */
	public function getNextPid($numPids, $pidNamespace)
	{
		$result = null;
		
		$parameters = array('numPIDs' => $numPids, 'pidNamespace' => $pidNamespace);
		
		$client = $this->getApiMSoapClient();
		$response = $client->getNextPID($parameters);
		if($response !== null)
		{
			$result = $response->pid;
		}
		
		return $result;
	}
	
	/**
	 * Ingest a new object.
	 * @param string $objectXml Object XML 
	 * @param string $format Object format
	 * @param string $logMessage Log message
	 * @return string The PID of the created object
	 */
	public function ingest($objectXml, $format, $logMessage = "ESCAPE API ingest")
	{
		$pid = null;
		
		$parameters = array('objectXML' => $objectXml, 'format' => $format, 'logMessage' => $logMessage);

		$client = $this->getApiMSoapClient();
		$response = $client->ingest($parameters);
		if($response !== null)
		{
			$pid = $response->objectPID;
		}
		
		return $pid;
	}
	
	/**
	 * Query the Resource Index using SPO.
	 * @param string $spoQuery SPO query
	 * @param string $format Result format, defaults to 'RDF/XML'
	 * @return string Query result
	 */
	public function queryRiSpo($spoQuery, $format = 'RDF/XML')
	{
		//TODO: error handling
		
		$url = $this->config['url'] . '/fedora/risearch';
		
		$formData = array(
						'type' => 'triples',
						'lang' => 'spo',
						'format' => $format,
						'query' => $spoQuery,
						'flush' => 'true'
					);
		
		try
		{
			$result = httputils_HttpUtils::doPostRequest($url, http_build_query($formData), null, $this->config['username'], $this->config['password']);
		}
		catch(Exception $exception)
		{
			throw new fedora_exception_QueryFailed($exception->getMessage(), $exception->getCode());
		}
		
		return $result;
	}

	/**
	 * Query the Resource Index using SPARQL.
	 * @param string $sparqlQuery SPARQL query
	 * @return array Array of result rows, each containing an array with tuple name/value pairs
	 */
	public function queryRiSparql($sparqlQuery)
	{
		$result = array();

		//TODO: error handling
		
		$url = $this->config['url'] . '/fedora/risearch';
		
		$formData = array(
						'type' => 'tuples',
						'lang' => 'sparql',
						'format' => 'CSV',
						'query' => $sparqlQuery,
						'flush' => 'true'
					);
		
		try
		{
			$handle = httputils_HttpUtils::doPostRequestFilePointer($url, http_build_query($formData), null, $this->config['username'], $this->config['password']);
			
			$fieldLabels = fgetcsv($handle);
			while(($resultLineArray = fgetcsv($handle)) !== FALSE)
			{
				$resultItem = array();
				foreach($resultLineArray as $key => $value)
				{
					$resultItem[$fieldLabels[$key]] = $value;
				}
				$result[] = $resultItem;
			}
			fclose($handle);
		}
		catch(Exception $exception)
		{
			throw new fedora_exception_QueryFailed($exception->getMessage(), $exception->getCode());
		}
		
		return $result;
	}

	/**
	 * Query the Resource Index using SPARQL.
	 * @param string $sparqlQuery SPARQL query
	 * @param string $template Template
	 * @param string $format Result format, defaults to 'RDF/XML'
	 * @return string Result
	 */
	public function queryRiSparqlWithTemplate($sparqlQuery, $template, $format = "RDF/XML")
	{
		$result = '';

		//TODO: error handling
		
		$url = $this->config['url'] . '/fedora/risearch';
		
		$formData = array(
						'type' => 'triples',
						'lang' => 'sparql',
						'format' => $format,
						'query' => $sparqlQuery,
						'template' => $template,
						'flush' => 'true'
					);
		
		try
		{
			$result = httputils_HttpUtils::doPostRequest($url, http_build_query($formData));
		}
		catch(Exception $exception)
		{
			throw new fedora_exception_QueryFailed($exception->getMessage(), $exception->getCode());
		}
		
		return $result;
	}
	
	/**
	 * Validates a PID.
	 * @param string $pid PID
	 * @return boolean TRUE if the PID specified is valid
	 */
	public function isValidPid($pid)
	{
		$isValid = false;
		
		if(preg_match('/^([A-Za-z0-9]|-|\\.)+:(([A-Za-z0-9])|-|\\.|~|_|(%[0-9A-F]{2}))+$/', $pid) && strlen($pid) <= 64)
		{
			$isValid = true;
		}
		
		return $isValid;
	}
	
	/**
	 * Convert a PID to an internal object URI.
	 * @param string $pid Object PID
	 * @return string Object URI
	 */
	public function convertPidToUri($pid)
	{
		if(!$this->isValidPid($pid))
		{
			throw new fedora_exception_InvalidFedoraPid('The PID specified is not a valid PID.');
		}
		return 'info:fedora/' . $pid;
	}

	/**
	 * Convert an internal object URI to a PID.
	 * @param string $uri Internal object URI
	 * @return string Object PID
	 */
	public function convertUriToPid($uri)
	{
		$pid = str_replace('info:fedora/', '', $uri);
		
		if($pid == $uri)
		{
			throw new fedora_exception_InvalidFedoraUri("URI does not match 'info:fedora/' structure");
		}
		else
		{
			if(!$this->isValidPid($pid))
			{
				throw new fedora_exception_InvalidFedoraPid('The PID specified is not a valid PID.');
			}
		}
		
		return $pid;
	}
}